﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Runtime.Remoting.Contexts;
using System.Runtime.Serialization.Formatters.Binary;

namespace SampleCode.VirtualFS
{
    //TODO: implement custom exceptions

    /// <summary>
    /// A facade for VirtualFS. Thread-safe.
    /// </summary>
    [Synchronization]
    public class FsDisk : ContextBoundObject, IDisposable
    {
        // Fields

        /// <summary>
        /// Stream to read/write the physical file
        /// </summary>
        private Stream _physicalStream;

        /// <summary>
        /// Do we need to dispose the stream when exiting
        /// </summary>
        private readonly bool _needToDisposePhysicalStream;

        /// <summary>
        /// Is async operation executing currently
        /// </summary>
        private bool _asyncOpInProgress;

        /// <summary>
        /// A disk the data is copied from asynchronously. Should be disposed when async operation completes
        /// </summary>
        private FsDisk _thatDisk;

        /// <summary>
        /// File and folder table. Stored at the beginning of the physical file
        /// </summary>
        internal FsFileTable FileTable;

        /// <summary>
        /// Path separator char used outside FsDisk
        /// </summary>
        public const char ExternalPathSeparator = '\\';

        /// <summary>
        /// The ctor opens a physical file with VirtualFS or creates a new one
        /// </summary>
        /// <param name="physicalFileName"></param>
        public FsDisk(string physicalFileName)
        {
            _needToDisposePhysicalStream = true;
            LoadOrCreateFileTable
            (
                new FileStream(physicalFileName, FileMode.OpenOrCreate, FileAccess.ReadWrite)
            );
        }

        /// <summary>
        /// The ctor allows for making a virtual file system upon any Stream
        /// </summary>
        /// <param name="physicalStream">Any Stream with read/write access</param>
        public FsDisk(Stream physicalStream)
        {
            LoadOrCreateFileTable(physicalStream);
        }

        ~FsDisk()
        {
            Dispose(false);
        }

        // Public members

        private readonly FsFolderInfo _rootFolder = new FsFolderInfo( new string(ExternalPathSeparator, 1) );
        /// <summary>
        /// Root folder info, just for consistency
        /// </summary>
        public FsFolderInfo RootFolder
        {
            get { return _rootFolder; }
        }

        /// <summary>
        /// Returns a current physical file size
        /// </summary>
        public int PhysicalLength
        {
            get
            {
                checked
                {
                    return (int)_physicalStream.Length;
                }
            }
        }

        /// <summary>
        /// Occurs when a folder is created
        /// </summary>
        public event FsFolderChangedEventHandler FolderCreated;
        /// <summary>
        /// Occurs when a folder is removed
        /// </summary>
        public event FsFolderRemovedEventHandler FolderRemoved;

        /// <summary>
        /// Occurs when a file is created
        /// </summary>
        public event FsFileChangedEventHandler FileCreated;
        /// <summary>
        /// Occurs when a file is removed
        /// </summary>
        public event FsFileRemovedEventHandler FileRemoved;
        /// <summary>
        /// Occurs when a file is changed
        /// </summary>
        public event FsFileChangedEventHandler FileChanged;

        /// <summary>
        /// Occurs when an asynchronous operation finishes or fails.
        /// In a real-world app specific delegates should be defined for each Async-method, of course.
        /// </summary>
        public event FsAsyncCompletedEventHandler AsyncOperationCompleted;

        /// <summary>
        /// Occurs when an asychronous operation has progress
        /// The handler should return true to cancel the operation
        /// </summary>
        public event FsProgressChangedEventHandler AsyncOperationProgress;

        /// <summary>
        /// Creates a folder
        /// </summary>
        /// <param name="folderName">UNC full path for the newly created folder</param>
        public void CreateFolder(string folderName)
        {
            FileTable.CreateFolder(folderName, true);
            OnFolderCreated(folderName);
        }

        /// <summary>
        /// Removes a folder
        /// </summary>
        /// <param name="folderName">UNC full path for the folder to remove</param>
        public void RemoveFolder(string folderName)
        {
            FileTable.RemoveFolder(folderName);
            OnFolderRemoved(folderName);
        }

        /// <summary>
        /// Renames a folder
        /// </summary>
        /// <param name="oldName">UNC full path for the folder to rename</param>
        /// <param name="newName">New folder name</param>
        public void RenameFolder(string oldName, string newName)
        {
            FileTable.RenameFolder(oldName, newName);
            OnFolderRemoved(oldName);
            OnFolderCreated(newName);
        }

        /// <summary>
        /// Copies a folder
        /// </summary>
        /// <param name="oldName">UNC full path to the existing folder</param>
        /// <param name="newName">New folder name</param>
        public void CopyFolder(string oldName, string newName)
        {
            // this also checks that the path is valid
            FileTable.CreateFolder(newName, false);
            OnFolderCreated(newName);

            Copy(this, oldName, this, newName, null);
        }

        /// <summary>
        /// Creates a new file
        /// </summary>
        /// <param name="fullName">UNC full path to the new file</param>
        public void CreateFile(string fullName)
        {
            var fileInfo = FileTable.CreateFile(fullName);
            OnFileCreated(fullName, fileInfo);
        }

        /// <summary>
        /// Opens a file for reading or writing
        /// </summary>
        /// <param name="fullName">UNC full path to the file</param>
        /// <param name="isForWriting">true - for writing, false - for reading</param>
        /// <returns>A thread-safe FsFileStream object</returns>
        public FsFileStream OpenFile(string fullName, bool isForWriting)
        {
            var stream = new FsFileStream
            (
                new FsSyncFileStream(_physicalStream, FileTable.GetFile(fullName), FileTable, isForWriting),
                fullName
            );

            stream.FileChanged += FileChanged;

            return stream;
        }

        /// <summary>
        /// Removes a file
        /// </summary>
        /// <param name="fullName">UNC full path to the file</param>
        public void RemoveFile(string fullName)
        {
            FileTable.RemoveFile(fullName);
            OnFileRemoved(fullName);
        }

        /// <summary>
        /// Renames/moves a file
        /// </summary>
        /// <param name="oldName">UNC full path to the file to rename</param>
        /// <param name="newName">New file name</param>
        public void RenameFile(string oldName, string newName)
        {
            FileTable.RenameFile(oldName, newName);
            OnFileRemoved(oldName);
            OnFileCreated(newName, FileTable.GetFile(newName));
        }

        /// <summary>
        /// Copies a file
        /// </summary>
        /// <param name="oldName">UNC full path to the existing file</param>
        /// <param name="newName">New file name</param>
        public void CopyFile(string oldName, string newName)
        {
            using(var fs = OpenFile(oldName, false))
            {
                CreateFile(newName);
                using (var fsNew = OpenFile(newName, true))
                {
                    fs.CopyTo(fsNew);
                }
            }
        }

        /// <summary>
        /// Enumerates subfolders
        /// </summary>
        /// <param name="folderName">Parent folder name</param>
        /// <param name="recursive">Return all gradchildren either?</param>
        /// <returns>IEnumerable of FsFolderInfo</returns>
        public IEnumerable<FsFolderInfo> GetSubFolders(string folderName, bool recursive)
        {
            return FileTable.GetSubFolders(folderName, recursive);
        }

        /// <summary>
        /// Enumerates subfolders
        /// </summary>
        /// <param name="folderName">Parent folder name</param>
        /// <returns>IEnumerable of FsFolderInfo</returns>
        public IEnumerable<FsFolderInfo> GetSubFolders(string folderName)
        {
            return FileTable.GetSubFolders(folderName, false);
        }

        /// <summary>
        /// Returns files from a folder
        /// </summary>
        /// <param name="folderName">Folder name</param>
        /// <returns>IEnumerable of FsFileInfo</returns>
        public IEnumerable<FsFileInfo> GetFiles(string folderName)
        {
            return FileTable.GetFiles(folderName);
        }

        /// <summary>
        /// Finds files matching a pattern in a folder and it's subfolders
        /// </summary>
        /// <param name="folderName">Folder name</param>
        /// <param name="pattern">A pattern to match. Can contain * and ? metacharacters</param>
        /// <returns>IEnumerable of FsFolderInfo</returns>
        public IEnumerable<FsFileInfo> FindFiles(string folderName, string pattern)
        {
            return FileTable.FindFiles(folderName, pattern);
        }

        /// <summary>
        /// Imports a physical folder with files
        /// Is static in order not to block FsDisk instance for the whole operation.
        /// Can fail in the middle, if something has changed from another thread.
        /// </summary>
        /// <param name="physicalFolderName">Full path to physical folder on disk</param>
        /// <param name="targetDisk">FsDisk to import into</param>
        /// <param name="targetFolderName">UNC full path to destination folder</param>
        /// <param name="cancelFunc">A predicate, that is called periodically to check if operation was cancelled</param>
        /// <returns>true if operation was cancelled</returns>
        public static bool Import(string physicalFolderName, FsDisk targetDisk, string targetFolderName, Func<string, bool> cancelFunc)
        {
            if (targetFolderName == null)
                targetFolderName = @"\";

            var parentDirInfo = new DirectoryInfo(physicalFolderName);

            // importing files
            if(ImportFiles(parentDirInfo, targetDisk, targetFolderName, cancelFunc))
                return true;

            // importing subfolders
            foreach (var dirInfo in parentDirInfo.EnumerateDirectories())
            {
                string folderName = targetFolderName + @"\" + dirInfo.Name;
                targetDisk.CreateFolder(folderName);

                if (Import(dirInfo.FullName, targetDisk, folderName, cancelFunc))
                    return true;
            }
            return false;
        }

        /// <summary>
        /// Imports a physical folder with files asynchronously
        /// </summary>
        /// <param name="physicalFolderName">Full path to physical folder on disk</param>
        /// <param name="targetFolderName">UNC full path to destination folder</param>
        /// <returns>Action to be executed to cancel execution</returns>
        public void ImportAsync(string physicalFolderName, string targetFolderName)
        {
            CheckAsyncState();

            var bw = new BackgroundWorker();

            bw.DoWork += (sender, e) => Import
            (
                physicalFolderName, 
                this, 
                targetFolderName,
                progressDesc => OnAsyncOperationProgress(progressDesc, e)
            );

            bw.RunWorkerCompleted += (sender, e) => OnAsyncOperationCompleted(e);

            bw.RunWorkerAsync();
        }

        /// <summary>
        /// Exports a folder with subfolders and files to a physical folder on disk
        /// Is static in order not to block FsDisk instance for the whole operation.
        /// Can fail in the middle, if something has changed from another thread.
        /// </summary>
        /// <param name="physicalFolderName">Destination folder on disk</param>
        /// <param name="sourceDisk">FsDisk to export from</param>
        /// <param name="sourceFolderName">UNC full path to source folder</param>
        /// <param name="cancelFunc">A predicate, that is called periodically to check if operation was cancelled</param>
        /// <returns>true if operation was cancelled</returns>
        public static bool Export(string physicalFolderName, FsDisk sourceDisk, string sourceFolderName, Func<string, bool> cancelFunc)
        {
            if(!Directory.Exists(physicalFolderName))
                throw new ArgumentException("The destination physical folder doesn't exist");

            if (sourceFolderName == null)
                sourceFolderName = @"\";

            // exporting files
            if (ExportFiles(physicalFolderName, sourceDisk, sourceFolderName, cancelFunc))
                return true;

            // exporting subfolders
            // copying subfolders
            foreach (var folderInfo in sourceDisk.GetSubFolders(sourceFolderName, true))
            {
                string targetChildFolderName = physicalFolderName + folderInfo.FullName.Substring(sourceFolderName.Length);

                Directory.CreateDirectory(targetChildFolderName);

                if (ExportFiles(targetChildFolderName, sourceDisk, folderInfo.FullName, cancelFunc))
                    return true;
            }

            return false;
        }

        /// <summary>
        /// Exports a folder with subfolders and files to a physical folder on disk asynchronously
        /// </summary>
        /// <param name="physicalFolderName">Destination folder on disk</param>
        /// <param name="sourceFolderName">UNC full path to source folder</param>
        public void ExportAsync(string physicalFolderName, string sourceFolderName)
        {
            CheckAsyncState();

            var bw = new BackgroundWorker();

            bw.DoWork += (sender, e) => Export
            (
                physicalFolderName, 
                this, 
                sourceFolderName,
                progressDesc => OnAsyncOperationProgress(progressDesc, e)
            );

            bw.RunWorkerCompleted += (sender, e) => OnAsyncOperationCompleted(e);

            bw.RunWorkerAsync();
        }

        /// <summary>
        /// Copies files and folders from one FsDisk to another.
        /// Is static in order not to block FsDisk instance for the whole operation.
        /// Can fail in the middle, if something has changed from another thread.
        /// </summary>
        /// <param name="sourceDisk">FsDisk to take from</param>
        /// <param name="sourceFolderName">UNC full path in sourceDisk</param>
        /// <param name="targetDisk">FsDisk to put into</param>
        /// <param name="targetFolderName">UNC full destination path in targetDisk</param>
        /// <param name="cancelFunc">A predicate, that is called periodically to check if operation was cancelled</param>
        /// <returns>true if operation was cancelled</returns>
        public static bool Copy(FsDisk sourceDisk, string sourceFolderName, FsDisk targetDisk, string targetFolderName, Func<string, bool> cancelFunc)
        {
            if (sourceFolderName == null)
                sourceFolderName = @"\";
            if (targetFolderName == null)
                targetFolderName = @"\";

            // copying files
            if (CopyFiles(sourceDisk, sourceFolderName, targetDisk, targetFolderName, cancelFunc))
                return true;

            // copying subfolders
            foreach (var folderInfo in sourceDisk.GetSubFolders(sourceFolderName, true))
            {
                string targetChildFolderName = targetFolderName + folderInfo.FullName.Substring(sourceFolderName.Length);
                
                targetDisk.CreateFolder(targetChildFolderName);

                if (CopyFiles(sourceDisk, folderInfo.FullName, targetDisk, targetChildFolderName, cancelFunc))
                    return true;
            }
            return false;
        }

        /// <summary>
        /// Copies files and folders from another FsDisk asynchronously
        /// </summary>
        /// <param name="sourceDisk">FsDisk to take from</param>
        /// <param name="sourceFolderName">UNC full path in sourceDisk</param>
        /// <param name="targetFolderName">UNC full destination path</param>
        public void CopyFromAsync(FsDisk sourceDisk, string sourceFolderName, string targetFolderName)
        {
            CheckAsyncState();

            var bw = new BackgroundWorker();

            bw.DoWork += (sender, e) => Copy
            (
                sourceDisk, 
                sourceFolderName, 
                this, 
                targetFolderName,
                progressDesc => OnAsyncOperationProgress(progressDesc, e)
            );

            bw.RunWorkerCompleted += (sender, e) => OnAsyncOperationCompleted(e);

            bw.RunWorkerAsync();
        }

        /// <summary>
        /// Copies files and folders from another FsDisk asynchronously. Disposes another FsDisk on completion
        /// </summary>
        /// <param name="sourcePhysicalFileName">Path to source physical file</param>
        /// <param name="sourceFolderName">UNC full path in sourceDisk</param>
        /// <param name="targetFolderName">UNC full destination path</param>
        public void CopyFromAsync(string sourcePhysicalFileName, string sourceFolderName, string targetFolderName)
        {
            _thatDisk = new FsDisk(sourcePhysicalFileName);
            CopyFromAsync(_thatDisk, sourceFolderName, targetFolderName);
        }

        /// <summary>
        /// Flushes changes to physical stream and closes it.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        private void Dispose(bool disposing)
        {
            // Check to see if Dispose has already been called.
            if (_physicalStream != null)
            {
                // If disposing equals true, dispose all managed
                // and unmanaged resources.
                if (disposing)
                {
                    // Dispose managed resources.
                    SaveFileTable();

                    if (_needToDisposePhysicalStream)
                        _physicalStream.Dispose();
                }

                // Call the appropriate methods to clean up
                // unmanaged resources here.
                // If disposing is false,
                // only the following code is executed.

                // Notice that disposing has been done.
                _physicalStream = null;
                FileTable = null;
            }
        }

        /// <summary>
        /// A static routine to copy files from one FsDisk to another
        /// </summary>
        /// <param name="sourceDisk"></param>
        /// <param name="sourceFolderName"></param>
        /// <param name="targetDisk"></param>
        /// <param name="targetFolderName"></param>
        /// <param name="cancelFunc">A predicate, that will be called periodically to check if operation is cancelled</param>
        /// <returns>true if operation was cancelled</returns>
        internal static bool CopyFiles(FsDisk sourceDisk, string sourceFolderName, FsDisk targetDisk, string targetFolderName, Func<string, bool> cancelFunc)
        {
            // copying files
            foreach (var fileInfo in sourceDisk.GetFiles(sourceFolderName))
            {
                if ((cancelFunc != null) && (cancelFunc(fileInfo.FullName)))
                    return true;

                using (var sourceFs = sourceDisk.OpenFile(fileInfo.FullName, false))
                {
                    string fileName = targetFolderName + @"\" + fileInfo.Name;
                    targetDisk.CreateFile(fileName);

                    using (var targetFs = targetDisk.OpenFile(fileName, true))
                    {
                        sourceFs.CopyTo(targetFs);
                    }
                }
            }
            return false;
        }

        /// <summary>
        /// A static routine to export files to a physical folder
        /// </summary>
        /// <param name="physicalFolderName"></param>
        /// <param name="sourceDisk"></param>
        /// <param name="sourceFolderName"></param>
        /// <param name="cancelFunc">A predicate, that will be called periodically to check if operation is cancelled</param>
        /// <returns>true if operation was cancelled</returns>
        internal static bool ExportFiles(string physicalFolderName, FsDisk sourceDisk, string sourceFolderName, Func<string, bool> cancelFunc)
        {
            foreach (var fileInfo in sourceDisk.GetFiles(sourceFolderName))
            {
                if ((cancelFunc != null) && (cancelFunc(fileInfo.FullName)))
                    return true;

                using (var targetFs = new FileStream(physicalFolderName + @"\" + fileInfo.Name, FileMode.CreateNew, FileAccess.Write))
                {
                    using(var sourceFs = sourceDisk.OpenFile(fileInfo.FullName, false))
                    {
                        sourceFs.CopyTo(targetFs);
                    }
                }
            }
            return false;
        }

        /// <summary>
        /// A static routine to import files from a physical folder
        /// </summary>
        /// <param name="dirInfo"></param>
        /// <param name="targetDisk"></param>
        /// <param name="folderName"></param>
        /// <param name="cancelFunc">A predicate, that will be called periodically to check if operation is cancelled</param>
        /// <returns>true if operation was cancelled</returns>
        internal static bool ImportFiles(DirectoryInfo dirInfo, FsDisk targetDisk, string folderName, Func<string, bool> cancelFunc)
        {
            foreach (var fileInfo in dirInfo.EnumerateFiles())
            {
                if ((cancelFunc != null) && (cancelFunc(fileInfo.FullName)))
                    return true;

                string fileName = folderName + @"\" + fileInfo.Name;
                targetDisk.CreateFile(fileName);

                using (var fsStream = targetDisk.OpenFile(fileName, true))
                {
                    using (var physicalStream = fileInfo.OpenRead())
                    {
                        physicalStream.CopyTo(fsStream);
                    }
                }
            }
            return false;
        }

        /// <summary>
        /// Saves the file table to disk using binary serialization
        /// </summary>
        internal void SaveFileTable()
        {
            if (FileTable == null)
                return;

            var memStream = new MemoryStream();
            new BinaryFormatter().Serialize(memStream, FileTable);

            if (memStream.Length > FileTable.DataStartOffset)
            {
                // if there're no files
                if(FileTable.FileList.First == null)
                {
                    // then simply increasing the DataStartOffset
                    checked
                    {
                        FileTable.DataStartOffset = (int)memStream.Length + FsFileTable.ReservedSpaceForGrowing;
                    }
                }
                else
                {
                    int newOffset;
                    checked
                    {
                        newOffset = (int)memStream.Length + FsFileTable.ReservedSpaceForGrowing;
                    }

                    // increasing the header size by moving some files to the end
                    MoveNextFilesToTheEnd
                    (
                        FileTable.FileList.First,
                        newOffset - FileTable.DataStartOffset,
                        _physicalStream,
                        FileTable
                    );

                    // correcting file start positions, because they're relative to DataStartOffset
                    foreach(var fileInfo in FileTable.FileList)
                    {
                        fileInfo.Start -= newOffset - FileTable.DataStartOffset;
                    }

                    FileTable.DataStartOffset = newOffset;
                }

                // reserializing
                memStream = new MemoryStream();
                new BinaryFormatter().Serialize(memStream, FileTable);
            }

            memStream.Position = 0;
            _physicalStream.Position = 0;
            memStream.CopyTo(_physicalStream);
        }

        /// <summary>
        /// Initializes PhysicalStream and loads or creates new file table 
        /// </summary>
        /// <param name="physicalStream"></param>
        private void LoadOrCreateFileTable(Stream physicalStream)
        {
            _physicalStream = physicalStream;

            if (_physicalStream.Length == 0)
            {
                // file is empty - creating a new file system
                FileTable = new FsFileTable();
            }
            else
            {
                // loading file system from file
                FileTable = new BinaryFormatter().Deserialize(_physicalStream) as FsFileTable;
                if (FileTable == null)
                    throw new FileFormatException("Physical stream has an invalid format");
            }
        }

        /// <summary>
        /// Moves files beginning from startNode to the end of the file to free spaceMissing space
        /// </summary>
        /// <param name="startNode"></param>
        /// <param name="spaceMissing"></param>
        /// <param name="physicalStream"></param>
        /// <param name="fileTable"></param>
        internal static void MoveNextFilesToTheEnd(LinkedListNode<FsInternalFileInfo> startNode, int spaceMissing, Stream physicalStream, FsFileTable fileTable)
        {
            var curNode = startNode;
            Debug.Assert(curNode != null);

            int filesToMoveCount = 0;
            int curSpaceMissing = spaceMissing;

            // getting a list of files to move by iterating through the list of next files
            while
            (
                (curNode != null)
                &&
                (curSpaceMissing > 0)
            )
            {
                int curFileCapacity = curNode.Value.Length;
                if (curNode.Next != null)
                    curFileCapacity = curNode.Next.Value.Start - curNode.Value.Start;

                curSpaceMissing -= curFileCapacity;

                filesToMoveCount++;
                curNode = curNode.Next;
            }
            Debug.Assert(filesToMoveCount > 0);

            // this will store the file next to the last file to be moved
            var nextNode = curNode;
            curNode = startNode;
            Debug.Assert(curNode != null);

            int newStart = fileTable.FileList.Last.Value.End + FsFileTable.ReservedSpaceForGrowing;
            if (newStart < (curNode.Value.Start + spaceMissing))
                newStart = curNode.Value.Start + spaceMissing;

            int moveOffset = newStart - curNode.Value.Start;

            // if there's no file next to the last file, that should be moved
            if (nextNode == null)
            {
                // then moving all the files by an offset
                CopyData
                (
                    curNode.Value.Start,
                    fileTable.FileList.Last.Value.End - curNode.Value.Start,
                    newStart,
                    physicalStream,
                    fileTable.DataStartOffset
                );

                while (curNode != null)
                {
                    curNode.Value.Start += moveOffset;
                    curNode = curNode.Next;
                }
            }
            else
            {
                // now moving some files to the end
                Debug.Assert(nextNode != null);

                CopyData
                (
                    curNode.Value.Start,
                    nextNode.Value.Start - curNode.Value.Start,
                    newStart,
                    physicalStream,
                    fileTable.DataStartOffset
                );

                while (curNode != nextNode)
                {
                    curNode.Value.Start += moveOffset;

                    var tempNode = curNode;
                    curNode = curNode.Next;

                    //TODO: this might be inefficient, as it needs to iterate through list - better try to just to switch node pointers
                    curNode.List.Remove(tempNode);
                    curNode.List.AddLast(tempNode);
                }
            }
        }

        /// <summary>
        /// Performs a block-by-block copying
        /// </summary>
        /// <param name="oldPos"></param>
        /// <param name="length"></param>
        /// <param name="newPos"></param>
        /// <param name="physicalStream"></param>
        /// <param name="dataStartOffset">The position where data actually starts in the stream. FsFileTable.DataStartOffset should be passed.</param>
        internal static void CopyData(int oldPos, int length, int newPos, Stream physicalStream, int dataStartOffset)
        {
            Debug.Assert(oldPos + length <= newPos);
            if ((length <= 0) || (oldPos == newPos))
                return;

            oldPos += dataStartOffset;
            newPos += dataStartOffset;

            int bufSize = FsFileTable.BlockCopyBufferSize < length ? FsFileTable.BlockCopyBufferSize : length;
            var buf = new byte[bufSize];

            while (length > 0)
            {
                physicalStream.Position = oldPos;
                if (physicalStream.Read(buf, 0, bufSize) < bufSize)
                    throw new IOException("Failed to read data from physical file");

                physicalStream.Position = newPos;
                physicalStream.Write(buf, 0, bufSize);

                oldPos += bufSize;
                newPos += bufSize;
                length -= bufSize;
            }
        }

        /// <summary>
        /// A helper for FolderCreated event
        /// </summary>
        /// <param name="fullName"></param>
        private void OnFolderCreated(string fullName)
        {
            if (FolderCreated != null)
                FolderCreated(this, new FsFolderChangedEventArgs(new FsFolderInfo(fullName)));
        }

        /// <summary>
        /// A helper for FolderRemoved event
        /// </summary>
        /// <param name="fullName"></param>
        private void OnFolderRemoved(string fullName)
        {
            if (FolderRemoved != null)
                FolderRemoved(this, new FsFolderRemovedEventArgs(fullName));
        }

        /// <summary>
        /// A helper for FileCreated event
        /// </summary>
        /// <param name="fullName"></param>
        /// <param name="fileInfo"></param>
        private void OnFileCreated(string fullName, FsInternalFileInfo fileInfo)
        {
            if (FileCreated != null)
                FileCreated(this, new FsFileChangedEventArgs(new FsFileInfo(fullName, fileInfo)));
        }

        /// <summary>
        /// A helper for FileRemoved event
        /// </summary>
        /// <param name="fullName"></param>
        private void OnFileRemoved(string fullName)
        {
            if (FileRemoved != null)
                FileRemoved(this, new FsFileRemovedEventArgs(fullName));
        }

        /// <summary>
        /// A helper to call AsyncOperationCompleted event
        /// </summary>
        /// <param name="e"></param>
        private void OnAsyncOperationCompleted(AsyncCompletedEventArgs e)
        {
            _asyncOpInProgress = false;

            if (_thatDisk != null)
                _thatDisk.Dispose();
            _thatDisk = null;

            if (AsyncOperationCompleted != null)
                AsyncOperationCompleted(this, e);
        }

        /// <summary>
        /// A helper to process async operation progresses
        /// </summary>
        /// <param name="progressDesc"></param>
        /// <param name="e"></param>
        /// <returns></returns>
        private bool OnAsyncOperationProgress(string progressDesc, CancelEventArgs e)
        {
            bool bCancel =
            (
                (AsyncOperationProgress != null)
                &&
                AsyncOperationProgress(this, new FsProgressChangedEventArgs(progressDesc))
            );

            if (bCancel)
                e.Cancel = true;

            return bCancel;
        }

        /// <summary>
        /// Checks if another async operation is not yet started
        /// </summary>
        private void CheckAsyncState()
        {
            if (_asyncOpInProgress)
                throw new InvalidOperationException("Another async operation is already in progress");
            _asyncOpInProgress = true;            
        }

#if DEBUG

        public void TraceFolders(string parentFolder)
        {
            Trace.WriteLine(parentFolder);

            foreach (var folder in GetSubFolders(parentFolder))
            {
                TraceFolders(folder.FullName);
            }

            foreach (var fileInfo in GetFiles(parentFolder))
                Trace.WriteLine(fileInfo.FullName + "(" + fileInfo + ")");
        }

        public void TraceDisk()
        {
            Trace.WriteLine("# Folders #");
            TraceFolders(@"\");

            FileTable.TraceFileList();
        }

#endif

    }
}
